package com.xhj.mybatis.executor.keygen;

import com.xhj.mybatis.binding.MapperMethod.ParamMap;
import com.xhj.mybatis.executor.Executor;
import com.xhj.mybatis.mapping.MappedStatement;
import com.xhj.mybatis.reflection.ArrayUtil;
import com.xhj.mybatis.reflection.MetaObject;
import com.xhj.mybatis.reflection.ParamNameResolver;
import com.xhj.mybatis.session.Configuration;
import com.xhj.mybatis.type.JdbcType;
import com.xhj.mybatis.type.TypeHandler;
import com.xhj.mybatis.type.TypeHandlerRegistry;
import com.xhj.mybatis.util.MapUtil;

import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.*;

/**
 * Jdbc3KeyGenerator
 *
 * @author XJks
 * @description Jdbc3KeyGenerator 类实现了 KeyGenerator 接口，用于处理 JDBC3 方式生成的主键
 */
public class Jdbc3KeyGenerator implements KeyGenerator {

    /**
     * 用于标识第二个泛型参数的常量
     */
    private static final String SECOND_GENERIC_PARAM_NAME = ParamNameResolver.GENERIC_NAME_PREFIX + "2";

    /**
     * 共享的单例实例
     */
    public static final Jdbc3KeyGenerator INSTANCE = new Jdbc3KeyGenerator();

    /**
     * 键数量过多时的错误提示信息
     */
    private static final String MSG_TOO_MANY_KEYS = "Too many keys are generated. There are only %d target objects. "
            + "You either specified a wrong 'keyProperty' or encountered a driver bug like #1523.";

    @Override
    public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
        // 不做任何处理
    }

    @Override
    public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
        processBatch(ms, stmt, parameter);
    }

    /**
     * 键数量过多时的错误提示信息
     *
     * @param ms        SQL 映射语句
     * @param stmt      JDBC 语句
     * @param parameter 参数对象
     */
    public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {
        // 获取 Java 对象的主键属性名数组
        final String[] keyProperties = ms.getKeyProperties();
        // 如果 Java 对象的主键属性名数组为空，直接返回
        if (keyProperties == null || keyProperties.length == 0) {
            return;
        }
        // TODO:因为该方法 processBatch 是在 SQL 执行后调用的，此时可以通过 JDBC Statement 获取数据库生成的主键结果集
        try (ResultSet rs = stmt.getGeneratedKeys()) {
            // 获取主键结果集的元数据
            final ResultSetMetaData rsmd = rs.getMetaData();
            // 获取 MyBatis 配置
            final Configuration configuration = ms.getConfiguration();
            // TODO:判断配置的 Java 对象主键属性名数量是否超过数据库返回的主键列数量
            if (rsmd.getColumnCount() < keyProperties.length) {
                // 如果数据库返回的主键列数量小于 Java 对象的主键属性名数量，可能发生了错误，这里选择不处理
            } else {
                // 如果数据库返回的主键列数量大于等于 Java 对象的主键属性名数量，则进行主键值分配，将主键值分配到参数对象中
                assignKeys(configuration, rs, rsmd, keyProperties, parameter);
            }
        } catch (Exception e) {
            throw new RuntimeException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
        }
    }

    /**
     * 分配主键到参数对象，根据参数类型分不同方式处理
     *
     * @param configuration MyBatis 配置
     * @param rs            主键结果集
     * @param rsmd          主键结果集元数据
     * @param keyProperties 主键属性名数组
     * @param parameter     参数对象
     * @throws SQLException SQL 异常
     */
    private void assignKeys(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd, String[] keyProperties, Object parameter) throws SQLException {
        /**
         * TODO:
         *  传入的 parameter 是一个包装对象，该对象的包装过程如下:
         *      1.{@link com.xhj.mybatis.binding.MapperMethod.MethodSignature#convertArgsToSqlCommandParam(Object[] args)}
         *      2.{@link com.xhj.mybatis.session.defaults.DefaultSqlSession#wrapCollection(Object)}
         *      3.通过以上两个方法，最终将单参数或多参数进行包装，形成最终的 parameter 对象，目前这两个方法的最终包装类型只有两种:
         *          3.1 XxxMapper.class 对应方法参数列表只有一个参数，且该参数没有使用 @Param 注解修饰，则 parameter 就是该单参数对象本身。
         *          3.2 XxxMapper.class 对应方法参数列表有多个参数，则 parameter 就是一个 ParamMap 对象。
         */
        // 根据参数对象类型，选择不同的主键分配方式
        if (parameter instanceof ParamMap) {
            // 如果参数对象 parameter 是 ParamMap，则只有一个包装对象 ParamMap 需要填充主键
            assignKeysToParamMap(configuration, rs, rsmd, keyProperties, (Map<String, ?>) parameter);
        } else if (parameter instanceof ArrayList && !((ArrayList<?>) parameter).isEmpty() && ((ArrayList<?>) parameter).get(0) instanceof ParamMap) {
            /**
             * TODO:
             *  1.在源码中，该分支主要用于处理批量插入的场景，即 XxxMapper.class 对应方法参数是一个 List<XxxObject> 类型的集合，该集合中存放的元素类型是 XxxObject 类型。
             *  2.每个 XxxObject 对象表示一条待插入的记录，对应一个 ParamMap 对象，所以 parameter 实际上是一个 ArrayList<ParamMap<?>> 类型的集合。
             *  3.在批量插入时，JDBC 驱动可能会返回多个生成的主键值，这些主键值需要依次分配给参数列表中的每个 ParamMap 对象，在这里就调用 assignKeysToParamMapList 方法进行分配。
             *  4.该分支的执行相关类主要有 BatchExecutor 和 DefaultSqlSession。BatchExecutor 负责批量执行 SQL 语句，DefaultSqlSession 负责调用 BatchExecutor 执行批量插入操作，目前 {@link com.xhj.mybatis.session.SqlSession} 并没有实现相关的批量插入方法，所以该分支暂时无法执行到。
             *  5.总之该分支的处理的 ArrayList<ParamMap<?>> 类型的参数对象，则有多个包装对象 ParamMap 对象分配主键值。
             */
            assignKeysToParamMapList(configuration, rs, rsmd, keyProperties, (ArrayList<ParamMap<?>>) parameter);
        } else {
            // 如果参数对象 parameter 是单参数对象，且没有使用 @Param 注解，则有一个 "普通对象" 需要填充主键
            assignKeysToParam(configuration, rs, rsmd, keyProperties, parameter);
        }
    }

    /**
     * 将主键分配给"单参数对象"，即普通 Java 对象，todo:可能还会处理批量操作时的普通对象数组或集合，待进一步学习确认。
     *
     * @param configuration MyBatis 配置
     * @param rs            主键结果集
     * @param rsmd          主键结果集元数据
     * @param keyProperties 主键属性名数组
     * @param parameter     参数对象
     * @throws SQLException SQL 异常
     */
    private void assignKeysToParam(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd, String[] keyProperties, Object parameter) throws SQLException {
        /**
         * TODO:
         *  疑问:如果 parameter 是普通 Java 对象，为什么还要把它转换为集合来处理？
         *  可能的原因:
         *      1.该方法不仅会处理单个普通对象，还可能处理多个普通对象的情况，即对应 XxxMapper.class 对应方法的参数列表可能是一个普通对象数组或集合，示例代码如下:
         *          1.1.单个普通对象: {@code void insertUser(User user);}
         *          1.2.多个普通对象数组:{@code void insertUsers(User[] users);}
         *          1.3.多个普通对象集合:{@code void insertUsers(List<User> users);}
         *      2.通过 collectionize 方法将 parameter 转换为集合，可以统一处理单个对象和多个对象的情况，避免重复代码。
         *      3.如果 parameter 是单个普通对象，collectionize 方法会将其包装为一个只有一个元素的集合，这样后续的处理逻辑可以保持一致。
         *      4.如果 parameter 是多个普通对象的数组或集合，collectionize 方法会直接返回对应的集合，方便后续遍历处理。
         *      5.总之，通过将 parameter 转换为集合，可以简化代码逻辑，同时支持单个对象和多个对象的主键分配。
         *  上述原因的体现:
         *      1.调用 collectionize 方法将 parameter 转换为集合 params。
         *      2.使用 {@code while (rs.next())} 代码块遍历主键结果集，为集合中的每个普通对象分配主键值。
         *          2.1 如果 parameter 是单个普通对象，则只会执行一次循环，为该对象分配主键值。
         *          2.2 如果 parameter 是多个普通对象的数组或集合，则会多次执行循环，依次为每个对象分配主键值。
         */
        Collection<?> params = collectionize(parameter);
        if (params.isEmpty()) {
            return;
        }
        // 创建主键分配器列表
        List<KeyAssigner> assignerList = new ArrayList<>();
        // 遍历对应 Java 对象的主键属性名列表，创建对应的主键分配器并添加到列表中
        for (int i = 0; i < keyProperties.length; i++) {
            /**
             * TODO:
             *  1.因为 parameter 是普通 Java 对象，可能是集合类型，但不是 ParamMap 类型，所以 paramName 参数传 null
             *  2.总之，只有在处理 ParamMap 参数对象时，paramName 才会有值，其他情况都传 null。
             */
            assignerList.add(new KeyAssigner(configuration, rsmd, i + 1, null, keyProperties[i]));
        }
        // 将 parameter 对象集合化后的 params 对象转换为迭代器，便于逐个获取参数对象
        Iterator<?> iterator = params.iterator();
        // 遍历数据库表主键结果集
        while (rs.next()) {
            // 如果参数对象迭代器没有下一个元素，说明数据库表生成的主键数量超过参数对象数量，抛出异常
            if (!iterator.hasNext()) {
                throw new RuntimeException(String.format(MSG_TOO_MANY_KEYS, params.size()));
            }
            // 获取需要分配主键的参数对象
            Object param = iterator.next();
            // 使用所有主键分配器为该参数对象分配主键值
            assignerList.forEach(x -> x.assign(rs, param));
        }
    }

    /**
     * 批量操作时，将主键分配给 ArrayList<ParamMap<?>> 列表中的每个 ParamMap 对象
     *
     * @param configuration MyBatis 配置
     * @param rs            主键结果集
     * @param rsmd          主键结果集元数据
     * @param keyProperties 主键属性名数组
     * @param paramMapList  参数 Map 列表
     * @throws SQLException SQL 异常
     */
    private void assignKeysToParamMapList(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd, String[] keyProperties, ArrayList<ParamMap<?>> paramMapList) throws SQLException {
        // 获取参数列表的迭代器，用于依次处理每个 ParamMap
        Iterator<ParamMap<?>> iterator = paramMapList.iterator();
        // 用于存放主键分配器，每个主键属性对应一个分配器
        List<KeyAssigner> assignerList = new ArrayList<>();
        // 计数器，记录已分配主键的数量
        long counter = 0;
        // 遍历主键结果集，每次循环分配一个主键
        while (rs.next()) {
            // 如果参数迭代器没有下一个元素，说明主键数量超过参数对象数量，抛出异常
            if (!iterator.hasNext()) {
                throw new RuntimeException(String.format(MSG_TOO_MANY_KEYS, counter));
            }
            // 获取下一个参数对象（ParamMap），准备分配主键
            ParamMap<?> paramMap = iterator.next();
            // 如果主键分配器列表还没初始化，则为每个主键属性创建分配器
            if (assignerList.isEmpty()) {
                for (int i = 0; i < keyProperties.length; i++) {
                    // 创建主键分配器并添加到列表
                    assignerList.add(
                            getAssignerForParamMap(configuration, rsmd, i + 1, paramMap, keyProperties[i], keyProperties, false)
                                    .getValue()
                    );
                }
            }
            // 使用所有主键分配器为当前参数对象分配主键值
            assignerList.forEach(x -> x.assign(rs, paramMap));
            // 已分配主键数量加一
            counter++;
        }
    }

    /**
     * 将主键分配给 ParamMap 参数对象
     *
     * @param configuration MyBatis 配置对象，包含类型处理器等信息
     * @param rs            主键结果集，JDBC返回的自动生成主键
     * @param rsmd          主键结果集元数据，用于获取列类型等信息
     * @param keyProperties 主键属性名数组，对应实体类的主键字段
     * @param paramMap      参数 Map，存储方法参数名和参数值
     * @throws SQLException SQL 异常
     */
    private void assignKeysToParamMap(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd, String[] keyProperties, Map<String, ?> paramMap) throws SQLException {
        // 如果参数对象 paramMap 为空，不能进行属性填充，直接返回
        if (paramMap.isEmpty()) {
            return;
        }
        /**
         * TODO:
         *  assignerMap 结构分析:
         *    1.键: 参数名 String
         *    2.值: Map.Entry<Iterator<?>, List<KeyAssigner>>
         *       2.1.键: 参数对象迭代器 Iterator<?>，指向要回填的目标对象序列（可能是单个对象，也可能是一组对象，比如批量插入时的多条记录）
         *       2.2.值: 主键分配器列表 List<KeyAssigner>，针对这个参数名下，要回填的属性们（可能是单主键属性，也可能是复合主键的多个属性）
         */
        Map<String, Map.Entry<Iterator<?>, List<KeyAssigner>>> assignerMap = new HashMap<>();
        // 遍历所有主键属性
        for (int i = 0; i < keyProperties.length; i++) {
            // 为第 i 个 Java 对象主键属性名创建被 Entry 包装后的主键分配器 KeyAssigner 对象
            Map.Entry<String, KeyAssigner> entry = getAssignerForParamMap(configuration, rsmd, i + 1, paramMap, keyProperties[i], keyProperties, true);
            /**
             * TODO:
             *  1.根据参数名，从 assignerMap 中获取对应的 Entry 对象，如果不存在则创建一个新的 Entry 对象并放入 assignerMap 中，这个 Entry 对象类型为 Map.Entry<Iterator<?>, List<KeyAssigner>>
             *  2.在 {@link #getAssignerForParamMap} 方法对应的案例中，详情如下:
             *      2.1 假设有一个 XxxMapper.class 对应方法定义如下:
             *          {@code void insertUser(@Param("user") User user, @Param("role") String role);}
             *      2.2 假设对应 XxxMapper.xml 对应方法的标签定义如下:
             *          {@code <insert id="insertUser" useGeneratedKeys="true" key="user.id">}
             *      2.3 在执行该方法时，参数对象 parameter 会被包装为一个 ParamMap 对象，包含两个键值对:
             *          - key: "user", value: User 对象
             *          - key: "role", value: "admin"
             *          - key: "param1", value: User 对象
             *          - key: "param2", value: "admin"
             *      2.4 其中 ParamMap 中的 param1 和 param2 是根据参数位置自动生成的通用参数名。
             *  3.下方代码只会在处理第一个参数名时，才会创建新的 Entry 对象，后续相同参数名的主键属性会复用该 Entry 对象，并将对应的主键分配器添加到该 Entry 对象的主键分配器列表中。
             *  4.在上述案例中，在处理 property = "user.id" 主键属性时，参数名 paramName = "user"，会创建一个新的 Entry 对象，并将 User 对象的迭代器和主键分配器列表添加到该 Entry 对象中。
             *  5.如果后续还有其他主键属性，类似 "user.name" 之类的，也会使用相同的参数名 "user"，则会复用该 Entry 对象，并将对应的主键分配器添加到主键分配器列表中。
             */
            Map.Entry<Iterator<?>, List<KeyAssigner>> iteratorPair = MapUtil.computeIfAbsent(assignerMap, entry.getKey(), k -> MapUtil.entry(collectionize(paramMap.get(k)).iterator(), new ArrayList<>()));
            // 为对应参数名下的主键分配器列表添加新的主键分配器
            iteratorPair.getValue().add(entry.getValue());
        }
        // 计数器，记录已分配主键的数量
        long counter = 0;
        // 遍历主键结果集，每次循环分配一个主键
        while (rs.next()) {
            // 获取每个参数名对应的参数对象迭代器和主键分配器列表，进行主键分配
            for (Map.Entry<Iterator<?>, List<KeyAssigner>> pair : assignerMap.values()) {
                // 如果参数迭代器没有下一个元素，说明数据库生成的主键数量超过参数对象数量，抛出异常
                if (!pair.getKey().hasNext()) {
                    throw new RuntimeException(String.format(MSG_TOO_MANY_KEYS, counter));
                }
                // 获取下一个参数对象
                Object param = pair.getKey().next();
                // 对该参数对象，使用所有主键分配器分配主键
                pair.getValue().forEach(x -> x.assign(rs, param));
            }
            // 已分配主键数量加一
            counter++;
        }
    }

    /**
     * 获取参数类型为 ParamMap 时的主键分配器，用 Entry 包装参数名和主键分配器
     *
     * @param config         MyBatis 配置
     * @param rsmd           主键结果集元数据
     * @param columnPosition 主键列位置
     * @param paramMap       参数 Map
     * @param keyProperty    主键属性名
     * @param keyProperties  主键属性名数组
     * @param omitParamName  是否省略参数名
     * @return 主键分配器条目
     */
    private Map.Entry<String, KeyAssigner> getAssignerForParamMap(Configuration config, ResultSetMetaData rsmd, int columnPosition, Map<String, ?> paramMap, String keyProperty, String[] keyProperties, boolean omitParamName) {
        /**
         * TODO:
         *  案例分析:
         *    1 假设有一个 XxxMapper.class 对应方法定义如下:
         *          {@code void insertUser(@Param("user") User user, @Param("role") String role);}
         *    2 假设对应 XxxMapper.xml 对应方法的标签定义如下:
         *          {@code <insert id="insertUser" useGeneratedKeys="true" key="user.id">}
         *    3 在执行该方法时，参数对象 parameter 会被包装为一个 ParamMap 对象，包含两个键值对:
         *          - key: "user", value: User 对象
         *          - key: "role", value: "admin"
         *    4 此时，keyProperty 就是 "user.id"，keySet 就是 ["user", "role"]。
         */
        Set<String> keySet = paramMap.keySet();
        // 判断是否只有一个参数（没有 param2 这个特殊 key），todo:目前我对源码这个逻辑存在一些疑问。
        boolean singleParam = !keySet.contains(SECOND_GENERIC_PARAM_NAME);
        // 查找普通 Java 对象主键属性名中是否有点号（用于分隔参数名和属性名）
        int firstDot = keyProperty.indexOf('.');
        // TODO:如果没有点号，说明没有指定参数名，即只有属性名，但在该案例中，keyProperty 应该是 "user.id" 这种形式，所以不会进入该 if 分支，而是执行下面的逻辑。
        if (firstDot == -1) {
            // 如果只有一个参数
            if (singleParam) {
                // TODO:因为没有参数名，直接通过普通 Java 对象主键属性名去获取主键分配器，此时 keyProperty 就是属性名，没有参数名部分，没有点号
                return getAssignerForSingleParam(config, rsmd, columnPosition, paramMap, keyProperty, omitParamName);
            }
            // 如果有多个参数但没指定参数名，抛出异常
            throw new RuntimeException("Could not determine which parameter to assign generated keys to. "
                    + "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
                    + "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
                    + keySet);
        }
        // 有点号，分离出参数名，TODO:在该案例中，paramName 就是 "user"，需要从 paramMap 中找到该参数名对应的 User 对象。
        String paramName = keyProperty.substring(0, firstDot);
        // 参数 Map 中包含该参数名
        if (keySet.contains(paramName)) {
            // 是否忽略参数名
            String argParamName = omitParamName ? null : paramName;
            // 获取普通 Java 对象主键属性名
            String argKeyProperty = keyProperty.substring(firstDot + 1);
            /**
             * TODO:
             *  1.因为 paramName 对应的是一个普通 Java 对象，真正的主键属性名是在该对象内部的属性，且该对象内部可能存在多个主键属性，所以这里使用了 Entry 对 paramName 进行一层包装。
             *  2.在返回该 Entry 对象后，可能返回了同一个 paramName 对应的多个 KeyAssigner 对象，这时需要在外层进行收集处理。
             */
            return MapUtil.entry(paramName, new KeyAssigner(config, rsmd, columnPosition, argParamName, argKeyProperty));
        } else if (singleParam) {
            // 参数 Map 中不包含该参数名，但只有一个参数，仍然用单参数方式获取主键分配器
            return getAssignerForSingleParam(config, rsmd, columnPosition, paramMap, keyProperty, omitParamName);
        } else {
            // 参数 Map 中不包含该参数名，且有多个参数，抛出异常，提示参数名错误
            throw new RuntimeException("Could not find parameter '" + paramName + "'. "
                    + "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
                    + "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
                    + keySet);
        }
    }

    /**
     * 获取单参数的主键分配器
     *
     * @param config         MyBatis 配置
     * @param rsmd           主键结果集元数据
     * @param columnPosition 主键列位置
     * @param paramMap       参数 Map
     * @param keyProperty    主键属性名
     * @param omitParamName  是否省略参数名
     * @return 主键分配器条目
     */
    private Map.Entry<String, KeyAssigner> getAssignerForSingleParam(Configuration config, ResultSetMetaData rsmd, int columnPosition, Map<String, ?> paramMap, String keyProperty, boolean omitParamName) {
        // 获取参数Map中唯一的参数名（假设只有一个参数）
        String singleParamName = nameOfSingleParam(paramMap);
        // 根据是否省略参数名，决定argParamName的值
        String argParamName = omitParamName ? null : singleParamName;
        // 创建并返回一个Map.Entry对象，key为参数名，value为主键分配器
        return MapUtil.entry(singleParamName, new KeyAssigner(config, rsmd, columnPosition, argParamName, keyProperty));
    }

    /**
     * 获取单参数的参数名
     *
     * @param paramMap 参数 Map
     * @return 参数名
     */
    private static String nameOfSingleParam(Map<String, ?> paramMap) {
        // 只有一个参数，任意 key 都可以
        return paramMap.keySet().iterator().next();
    }

    /**
     * 将参数对象转为集合，方便统一处理
     *
     * @param param 参数对象
     * @return 参数集合
     */
    private static Collection<?> collectionize(Object param) {
        if (param instanceof Collection) {
            return (Collection<?>) param;
        } else if (param instanceof Object[]) {
            return Arrays.asList((Object[]) param);
        } else {
            return Arrays.asList(param);
        }
    }

    /**
     * KeyAssigner 内部类，负责将主键值分配到参数对象的属性
     */
    private class KeyAssigner {
        /**
         * MyBatis 配置
         */
        private final Configuration configuration;

        /**
         * 主键结果集元数据
         */
        private final ResultSetMetaData rsmd;

        /**
         * 类型处理器注册表
         */
        private final TypeHandlerRegistry typeHandlerRegistry;

        /**
         * 数据库表主键列位置
         */
        private final int columnPosition;

        /**
         * TODO:
         *  1.只有在处理 ParamMap 参数对象时，才会有值，其他情况为 null
         *  2.案例分析:
         *    2.1 假设有一个 XxxMapper.class 对应方法定义如下:
         *          {@code void insertUser(@Param("user") User user, @Param("role") String role);}
         *    2.2 假设对应 XxxMapper.xml 对应方法的标签定义如下:
         *          {@code <insert id="insertUser" useGeneratedKeys="true" key="user.id">}
         *    2.3 在执行该方法时，参数对象 parameter 会被包装为一个 ParamMap 对象，包含两个键值对:
         *          - key: "user", value: User 对象
         *          - key: "role", value: "admin"
         *    2.4 此时，KeyAssigner 的 paramName 就会被设置为 "user"，表示主键值需要分配给 ParamMap 中 key 为 "user" 的 User 对象，propertyName 则为 "id"，表示 User 对象的 id 属性。
         */
        private final String paramName;

        /**
         * Java 对象主键属性名
         */
        private final String propertyName;

        /**
         * 类型处理器
         */
        private TypeHandler<?> typeHandler;

        protected KeyAssigner(Configuration configuration, ResultSetMetaData rsmd, int columnPosition, String paramName, String propertyName) {
            super();
            this.configuration = configuration;
            this.rsmd = rsmd;
            this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            this.columnPosition = columnPosition;
            this.paramName = paramName;
            this.propertyName = propertyName;
        }

        /**
         * 分配主键值到参数对象
         *
         * @param rs    主键结果集
         * @param param 参数对象
         */
        protected void assign(ResultSet rs, Object param) {
            // 如果有参数名，说明 param 是 ParamMap，需要取出实际参数对象
            if (paramName != null) {
                param = ((ParamMap<?>) param).get(paramName);
            }
            // 创建元对象，便于反射操作
            MetaObject metaParam = configuration.newMetaObject(param);
            try {
                // 如果类型处理器为空，则尝试获取
                if (typeHandler == null) {
                    // 如果对应参数对象有对应属性的 setter 方法，则尝试获取类型处理器
                    if (metaParam.hasSetter(propertyName)) {
                        // 获取属性类型
                        Class<?> propertyType = metaParam.getSetterType(propertyName);
                        // 获取对应类型处理器
                        typeHandler = typeHandlerRegistry.getTypeHandler(propertyType, JdbcType.forCode(rsmd.getColumnType(columnPosition)));
                    } else {
                        throw new RuntimeException("No setter found for the keyProperty '" + propertyName + "' in '"
                                + metaParam.getOriginalObject().getClass().getName() + "'.");
                    }
                }
                if (typeHandler == null) {
                    // 没有类型处理器，可能有错误
                } else {
                    // 从结果集获取主键值
                    Object value = typeHandler.getResult(rs, columnPosition);
                    // 设置到参数对象属性
                    metaParam.setValue(propertyName, value);
                }
            } catch (SQLException e) {
                throw new RuntimeException("Error getting generated key or setting result to parameter object. Cause: " + e,
                        e);
            }
        }
    }
}
